import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;
import java.io.File;
import java.io.ObjectInputStream.GetField;
import java.sql.*;

import javax.sql.*;

import ncsa.hdf.object.h5.*;

public class KMeans {
	static DatabaseConnector dc = new DatabaseConnector();
	static Statement stmt;
	
	private static Random rand = new Random(123456789L);
	ArrayList<double[]> clustSum;
	
	private static double tempo;
	private static double duration;
	private static double loudness;
	private static double key;
	private static double mode;
	private static double year;
	private static double hotness;
	private static String title;
	private static String songID;
	
//	private static ArrayList<Song> songs;
	
	/* 
	 * 
	 * Assumptions:
	 * 1. The songs' properties already have their values normalized from 0 to 1.
	 * 2. Each song has 7 properties and can be accessed using the getProp method.
	 * 3. The songs' properties have all been normalized already.
	 * 
	 * Return the K-Means clusters for the songs.
	 */
	public static void main(String[] args) {
		ArrayList<Song> songs = new ArrayList<Song>();
		tempo = 0.0;
		duration = 0.0;
		loudness = 0.0;
		key = 0.0;
		mode = 0.0;
		year = 0.0;
		hotness = 0.0;
		title = "";
		songID = "";
		
		songs = new ArrayList<Song>();
		Statement stmt = dc.getStatement();
		try
    	{
			String sql;
			sql = "SELECT `Tempo`,`Duration`,`Loudness`,`Key`,`Mode`,`Year`,`Hotness`,`Name`,`SongID` FROM `song_recommendation`.`Song`";
			ResultSet rs = stmt.executeQuery(sql);
			while (rs.next()) 
			{
				duration = rs.getDouble("Duration");
				tempo = rs.getDouble("Tempo");
				loudness = rs.getDouble("Loudness");
				key = rs.getDouble("Key");
				mode = rs.getDouble("Mode");
				year = rs.getDouble("Year");
				hotness = rs.getDouble("Hotness");
				title = rs.getString("Name");
				songID = rs.getString("SongID");
				
				songs.add(new Song(tempo, duration, loudness, key, mode, year, hotness, title, songID));
			}
		} catch(SQLException se) {
			se.printStackTrace();
		}
		
//		String directory = args[0];
//		File aFile = new File(directory);
//		File[] listOfDirs = aFile.listFiles();
//		for (int j =0; j<listOfDirs.length; j++)
//		{
//			File[] listOfFiles = listOfDirs[j].listFiles();
//			for (int i = 0; i< listOfFiles.length; i++) {
//				String filename = listOfFiles[i].toString();
//				H5File h5 = new H5File(filename,H5File.READ);
//				title = hdf5_getters.get_member_string(h5,0,"/metadata/songs","title");
//				    
//			    hotness = hdf5_getters.get_member_double(h5,0,"/metadata/songs","song_hotttnesss");
//			    if (Double.isNaN( hotness ) )
//			    {
//			    	hotness = hdf5_getters.get_member_double(h5,0,"/metadata/songs","artist_hotttnesss");
//				    if (Double.isNaN( hotness ) )
//				    {
//				    	hotness= 0;
//				    }
//			    }
//			    
//			    duration = (float) hdf5_getters.get_member_double(h5,0,"/analysis/songs","duration");
//			    if (duration < 30)
//			    	duration = 0;
//		    	else if( duration > 600)
//		    		duration = 1;
//		    	else
//		    		duration = (duration - 30)/570; 
//
//	    		int keyInt = hdf5_getters.get_member_int(h5,0,"/analysis/songs","key");
// 				key = ((double) keyInt)/24;
//
//				    
//	    		loudness = hdf5_getters.get_member_double(h5,0,"/analysis/songs","loudness");
//	    		if (loudness > -5)
//	    			loudness = 0;
//	    		else if (loudness < -25 )
//	    			loudness = 1;
//	    		else
//	    			loudness = -( loudness + 5 ) / 20;
//
//
//	    		mode = (double) hdf5_getters.get_mode(h5);
//			        
//			    tempo = hdf5_getters.get_member_int(h5,0,"/analysis/songs","mode");
//			    if (tempo < 50)
//			    	tempo = 0;
//			    else if (tempo > 200)
//			    	tempo = 1;
//			    else
//			    	tempo = (tempo -50) / 150;
//			    
//			    year = (double) hdf5_getters.get_member_int(h5,0,"/musicbrainz/songs","year");
//			    if ( year < 1950 )
//			    	year = 0;
//			    else if (year > 2013)
//			    	year = 1;
//			    else
//			    	year = (year - 1950)/ 63;
//
//			    songID = hdf5_getters.get_member_string(h5,0,"/metadata/songs","song_id");
//			    
//			    songs.add( new Song( tempo, duration, loudness, key, mode, year, hotness, title, songID) );
//			    
//			    hdf5_getters.hdf5_close(h5);		
//			}
//		}
		System.out.println("STARTING TO CLUSTER...");
		int k = 10;
		HashMap<Integer,ArrayList<Song>> matches = new HashMap<Integer,ArrayList<Song>>();
		try {
			matches = findKMeans(k,3,songs);
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println("CLUSTERING DONE!");
	}
	
	public static HashMap<Integer, ArrayList<Song>> findKMeans(int k, int numIter, ArrayList<Song> songs) throws Exception {
		
		double[] means = new double[k];
//		means = kplusinit(k, songs); // use k-means++ to optimize cluster coefficients
//		for (int i=0; i<means.length; i++) {
//			System.out.println("mean "+i+": "+means[i]);
//		}
		HashMap<Integer,ArrayList<Song>> matches = new HashMap<Integer,ArrayList<Song>>(); // song matches for each cluster
		ArrayList<double[]> clustSum = new ArrayList<double[]>(); // sum for each property within a cluster
		double[] propMatchSums;
		// initially assign sums of each property to be 0 for each cluster
		// assign random means initially between 0 and 1, assume there are 7 properties for songs
		for (int i=0; i<k; i++) {
			means[i] = rand.nextDouble();
//			System.out.println(means[i]);
		}
//		System.out.println();
		double closestDist;
		double distance;
		int closestCluster;
		Song closest;
		Song closestSong;
		// find closest mean (from a cluster) to each of the songs and assign that song to that cluster
		//boolean change = true;
		for (int iter=0; iter<numIter; iter++) {
		//while (change)
			// clear
			propMatchSums = new double[]{0.0,0.0,0.0,0.0,0.0,0.0,0.0, 0};
			matches.clear();
			clustSum.clear();
			for (int i=0; i<k; i++) {
				matches.put(i, new ArrayList<Song>()); // initialize empty matches for each cluster
				clustSum.add(i, propMatchSums);
			}
			
			closestDist = 100;
			distance = 0;
			closestCluster = 0;
			closestSong = null;
			closest = null;
			
			// start iteration
			for (int n=0; n<songs.size(); n++) 
			{
				closestSong = songs.get(n);
				for (int p=0; p<k; p++) {
					// find euclidean distance: tempo, hotness, year, mode, key, duration, loudness
					distance = Math.sqrt(Math.pow(closestSong.getProp(0)-means[p],2) + Math.pow(closestSong.getProp(1)-means[p],2) + Math.pow(closestSong.getProp(2)-means[p],2) + Math.pow(closestSong.getProp(3)-means[p],2) + Math.pow(closestSong.getProp(4)-means[p],2) + Math.pow(closestSong.getProp(5)-means[p],2) + Math.pow(closestSong.getProp(6)-means[p],2));
					if (distance<closestDist) {
						closestDist = distance;
						closestCluster = p;
						closest = closestSong;
					}
				}
//				System.out.println("song "+n+" in "+closestCluster+ " with distance "+closestDist);
				ArrayList<Song> newClust = new ArrayList<Song>();
				newClust = matches.remove(closestCluster);
				newClust.add(closest);
				matches.put(closestCluster, newClust); // add matched song to corresponding cluster
				// update the sum for corresponding cluster
				propMatchSums = new double[]{(clustSum.get(closestCluster)[0]+closest.getProp(0)), (clustSum.get(closestCluster)[1]+closest.getProp(1)), (clustSum.get(closestCluster)[2]+closest.getProp(2)), (clustSum.get(closestCluster)[3]+closest.getProp(3)), (clustSum.get(closestCluster)[4]+closest.getProp(4)), (clustSum.get(closestCluster)[5]+closest.getProp(5)), (clustSum.get(closestCluster)[6]+closest.getProp(6)), clustSum.get(closestCluster)[7]+1};
				clustSum.set(closestCluster, propMatchSums);
				//for (int i=0; i<7; i++) System.out.println(propMatchSums[i]);
				//System.out.println();
				distance = 100;
			}
			
			double sum;
			// update the means to continue iterations (to achieve convergence)
			//double oldMeans;
			
			for (int i=0; i<k; i++) {
				//oldMeans = means[i];

				// compute the sum of within the cluster
				sum = clustSum.get(i)[0] + clustSum.get(i)[1] + clustSum.get(i)[2] + clustSum.get(i)[3] + clustSum.get(i)[4] + clustSum.get(i)[5] + clustSum.get(i)[6];
				
				// update the means
				double numMatch = clustSum.get(i)[7];
				//if (numMatch != 0) means[i] = sum/numMatch;
				if (numMatch != 0) means[i] = sum/numMatch/7; //System.out.println("old: "+oldMeans+", new: "+means[i]); }; //System.out.println(means[i]); System.out.println(sum); System.out.println(numMatch); System.out.println("cluster "+i); }
				else means[i] = rand.nextDouble();
				// if the clusters contain the same items and the corresponding means are unchanged, break out
				
				//System.out.println("cluster #"+i+": "+matches.get(i)+",sum: "+sum+",numMatch: "+numMatch+",means: "+means[i]+",oldmeans: "+oldMeans);
			}
			//System.out.println("\n====================================================");

			// clear
			//if (iter < numIter-1) {
//				propMatchSums = new double[]{0.0,0.0,0.0,0.0,0.0,0.0,0.0, 0};
//				matches.clear();
//				clustSum.clear();
//				for (int i=0; i<k; i++) {
//					matches.put(i, new ArrayList<Song>()); // initialize empty matches for each cluster
//					clustSum.add(i, propMatchSums);
//				}
			//}
		}
		
		// get only the filled clusters
		HashMap<Integer,ArrayList<Song>> fillClusters = new HashMap<Integer,ArrayList<Song>>();
		int y = 0;
		for (int i=0; i<k; i++) {
			if (matches.get(i).size() > 0) {
				fillClusters.put(y, matches.get(i));
				y++;
			}
		}

		try
    	{
			String sql;
			for (int j=0; j<fillClusters.size(); j++) {
				for (int q=0; q<fillClusters.get(j).size(); q++) {
					sql = "UPDATE `song_recommendation`.`Song` SET `Cluster`= ? WHERE `SongID`= ?";
					PreparedStatement updateQuery = dc.getConnection().prepareStatement(sql);
					updateQuery.setInt(1, j);
					updateQuery.setString(2, fillClusters.get(j).get(q).getID());
					updateQuery.executeUpdate();
				}
			}
		} catch(SQLException se) {
			se.printStackTrace();
		}
		
		// after all the iterations have completed, return the clusters with the filled songs
		return matches;
	}
	
	/* K-means++ initialization algorithm 
	 * http://en.wikipedia.org/wiki/K-means%2B%2B
	 */
	private double[] kplusinit(int k, ArrayList<Song> songs) throws Exception {
		Song randSong = songs.get(rand.nextInt(songs.size()));
		ArrayList<Double> distances = new ArrayList<Double>();
		double[] ks = new double[k];
		ks[0] = Math.sqrt(Math.pow(randSong.getProp(0),2) + Math.pow(randSong.getProp(1),2) + Math.pow(randSong.getProp(2),2) + Math.pow(randSong.getProp(3),2) + Math.pow(randSong.getProp(4),2) + Math.pow(randSong.getProp(5),2) + Math.pow(randSong.getProp(6),2));
		Song closestSong;
		double ranDist;
		for (int e=1; e<k; e++) {
			for (int i=0; i<songs.size(); i++) {
				closestSong = songs.get(i);
				distances.add(Math.sqrt(Math.pow(closestSong.getProp(0)-randSong.getProp(0),2) + Math.pow(closestSong.getProp(1)-randSong.getProp(1),2) + Math.pow(closestSong.getProp(2)-randSong.getProp(2),2) + Math.pow(closestSong.getProp(3)-randSong.getProp(3),2) + Math.pow(closestSong.getProp(4)-randSong.getProp(4),2) + Math.pow(closestSong.getProp(5)-randSong.getProp(5),2) + Math.pow(closestSong.getProp(6)-randSong.getProp(6),2)));
				if (distances.get(i) != 0) distances.set(i, distances.get(i)+Math.pow(distances.get(i), 2));
			}
			ranDist = distances.get(rand.nextInt(songs.size()));
			randSong = songs.get(distances.indexOf(ranDist));
			ks[e] = ranDist;
		}
		return ks;
	}

	/*
	 * A getter method to retrieve the averages of each of the properties within each cluster, which can be used for analysis purposes.
	 */
	public ArrayList<double[]> getClustAnalysis(int k, ArrayList<double[]> matches) {
		for (int i=0; i<k; i++) {
			
		}
		return clustSum;
	}
}
